// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/dom_storage/dom_storage_database.h"

#include "base/bind.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "sql/statement.h"
#include "sql/transaction.h"
#include "third_party/sqlite/sqlite3.h"

namespace {

const base::FilePath::CharType kJournal[] = FILE_PATH_LITERAL("-journal");

} // anon namespace

namespace content {

// static
base::FilePath DOMStorageDatabase::GetJournalFilePath(
    const base::FilePath& database_path)
{
    base::FilePath::StringType journal_file_name = database_path.BaseName().value() + kJournal;
    return database_path.DirName().Append(journal_file_name);
}

DOMStorageDatabase::DOMStorageDatabase(const base::FilePath& file_path)
    : file_path_(file_path)
{
    // Note: in normal use we should never get an empty backing path here.
    // However, the unit test for this class can contruct an instance
    // with an empty path.
    Init();
}

DOMStorageDatabase::DOMStorageDatabase()
{
    Init();
}

void DOMStorageDatabase::Init()
{
    failed_to_open_ = false;
    tried_to_recreate_ = false;
    known_to_be_empty_ = false;
}

DOMStorageDatabase::~DOMStorageDatabase()
{
    if (known_to_be_empty_ && !file_path_.empty()) {
        // Delete the db and any lingering journal file from disk.
        Close();
        sql::Connection::Delete(file_path_);
    }
}

void DOMStorageDatabase::ReadAllValues(DOMStorageValuesMap* result)
{
    if (!LazyOpen(false))
        return;

    sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE,
        "SELECT * from ItemTable"));
    DCHECK(statement.is_valid());

    while (statement.Step()) {
        base::string16 key = statement.ColumnString16(0);
        base::string16 value;
        statement.ColumnBlobAsString16(1, &value);
        (*result)[key] = base::NullableString16(value, false);
    }
    known_to_be_empty_ = result->empty();

    // Reduce the size of sqlite caches.
    db_->TrimMemory(false /* aggressively */);
}

bool DOMStorageDatabase::CommitChanges(bool clear_all_first,
    const DOMStorageValuesMap& changes)
{
    if (!LazyOpen(!changes.empty())) {
        // If we're being asked to commit changes that will result in an
        // empty database, we return true if the database file doesn't exist.
        return clear_all_first && changes.empty() && !base::PathExists(file_path_);
    }

    bool old_known_to_be_empty = known_to_be_empty_;
    sql::Transaction transaction(db_.get());
    if (!transaction.Begin())
        return false;

    if (clear_all_first) {
        if (!db_->Execute("DELETE FROM ItemTable"))
            return false;
        known_to_be_empty_ = true;
    }

    bool did_delete = false;
    bool did_insert = false;
    DOMStorageValuesMap::const_iterator it = changes.begin();
    for (; it != changes.end(); ++it) {
        sql::Statement statement;
        base::string16 key = it->first;
        base::NullableString16 value = it->second;
        if (value.is_null()) {
            statement.Assign(db_->GetCachedStatement(SQL_FROM_HERE,
                "DELETE FROM ItemTable WHERE key=?"));
            statement.BindString16(0, key);
            did_delete = true;
        } else {
            statement.Assign(db_->GetCachedStatement(SQL_FROM_HERE,
                "INSERT INTO ItemTable VALUES (?,?)"));
            statement.BindString16(0, key);
            statement.BindBlob(1, value.string().data(),
                value.string().length() * sizeof(base::char16));
            known_to_be_empty_ = false;
            did_insert = true;
        }
        DCHECK(statement.is_valid());
        statement.Run();
    }

    if (!known_to_be_empty_ && did_delete && !did_insert) {
        sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE,
            "SELECT count(key) from ItemTable"));
        if (statement.Step())
            known_to_be_empty_ = statement.ColumnInt(0) == 0;
    }

    bool success = transaction.Commit();
    if (!success)
        known_to_be_empty_ = old_known_to_be_empty;

    // Reduce the size of sqlite caches.
    db_->TrimMemory(false /* aggressively */);

    return success;
}

void DOMStorageDatabase::ReportMemoryUsage(
    base::trace_event::ProcessMemoryDump* pmd,
    const std::string& name)
{
    if (IsOpen())
        db_->ReportMemoryUsage(pmd, name);
}

bool DOMStorageDatabase::LazyOpen(bool create_if_needed)
{
    if (failed_to_open_) {
        // Don't try to open a database that we know has failed
        // already.
        return false;
    }

    if (IsOpen())
        return true;

    bool database_exists = base::PathExists(file_path_);

    if (!database_exists && !create_if_needed) {
        // If the file doesn't exist already and we haven't been asked to create
        // a file on disk, then we don't bother opening the database. This means
        // we wait until we absolutely need to put something onto disk before we
        // do so.
        return false;
    }

    db_.reset(new sql::Connection());
    db_->set_histogram_tag("DOMStorageDatabase");

    // This db does not use [meta] table, store mmap status data elsewhere.
    db_->set_mmap_alt_status();

    if (file_path_.empty()) {
        // This code path should only be triggered by unit tests.
        if (!db_->OpenInMemory()) {
            NOTREACHED() << "Unable to open DOM storage database in memory.";
            failed_to_open_ = true;
            return false;
        }
    } else {
        if (!db_->Open(file_path_)) {
            LOG(ERROR) << "Unable to open DOM storage database at "
                       << file_path_.value()
                       << " error: " << db_->GetErrorMessage();
            if (database_exists && !tried_to_recreate_)
                return DeleteFileAndRecreate();
            failed_to_open_ = true;
            return false;
        }
    }

    // sql::Connection uses UTF-8 encoding, but WebCore style databases use
    // UTF-16, so ensure we match.
    ignore_result(db_->Execute("PRAGMA encoding=\"UTF-16\""));

    if (!database_exists) {
        // This is a new database, create the table and we're done!
        if (CreateTableV2())
            return true;
    } else {
        // The database exists already - check if we need to upgrade
        // and whether it's usable (i.e. not corrupted).
        SchemaVersion current_version = DetectSchemaVersion();

        if (current_version == V2) {
            return true;
        } else if (current_version == V1) {
            if (UpgradeVersion1To2())
                return true;
        }
    }

    // This is the exceptional case - to try and recover we'll attempt
    // to delete the file and start again.
    Close();
    return DeleteFileAndRecreate();
}

DOMStorageDatabase::SchemaVersion DOMStorageDatabase::DetectSchemaVersion()
{
    DCHECK(IsOpen());

    // Connection::Open() may succeed even if the file we try and open is not a
    // database, however in the case that the database is corrupted to the point
    // that SQLite doesn't actually think it's a database,
    // sql::Connection::GetCachedStatement will DCHECK when we later try and
    // run statements. So we run a query here that will not DCHECK but fail
    // on an invalid database to verify that what we've opened is usable.
    if (db_->ExecuteAndReturnErrorCode("PRAGMA auto_vacuum") != SQLITE_OK)
        return INVALID;

    // Look at the current schema - if it doesn't look right, assume corrupt.
    if (!db_->DoesTableExist("ItemTable") || !db_->DoesColumnExist("ItemTable", "key") || !db_->DoesColumnExist("ItemTable", "value"))
        return INVALID;

    // We must use a unique statement here as we aren't going to step it.
    sql::Statement statement(
        db_->GetUniqueStatement("SELECT key,value from ItemTable LIMIT 1"));
    if (statement.DeclaredColumnType(0) != sql::COLUMN_TYPE_TEXT)
        return INVALID;

    switch (statement.DeclaredColumnType(1)) {
    case sql::COLUMN_TYPE_BLOB:
        return V2;
    case sql::COLUMN_TYPE_TEXT:
        return V1;
    default:
        return INVALID;
    }
}

bool DOMStorageDatabase::CreateTableV2()
{
    DCHECK(IsOpen());

    return db_->Execute(
        "CREATE TABLE ItemTable ("
        "key TEXT UNIQUE ON CONFLICT REPLACE, "
        "value BLOB NOT NULL ON CONFLICT FAIL)");
}

bool DOMStorageDatabase::DeleteFileAndRecreate()
{
    DCHECK(!IsOpen());
    DCHECK(base::PathExists(file_path_));

    // We should only try and do this once.
    if (tried_to_recreate_)
        return false;

    tried_to_recreate_ = true;

    // If it's not a directory and we can delete the file, try and open it again.
    if (!base::DirectoryExists(file_path_) && sql::Connection::Delete(file_path_)) {
        return LazyOpen(true);
    }

    failed_to_open_ = true;
    return false;
}

bool DOMStorageDatabase::UpgradeVersion1To2()
{
    DCHECK(IsOpen());
    DCHECK(DetectSchemaVersion() == V1);

    sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE,
        "SELECT * FROM ItemTable"));
    DCHECK(statement.is_valid());

    // Need to migrate from TEXT value column to BLOB.
    // Store the current database content so we can re-insert
    // the data into the new V2 table.
    DOMStorageValuesMap values;
    while (statement.Step()) {
        base::string16 key = statement.ColumnString16(0);
        base::NullableString16 value(statement.ColumnString16(1), false);
        values[key] = value;
    }

    sql::Transaction migration(db_.get());
    return migration.Begin() && db_->Execute("DROP TABLE ItemTable") && CreateTableV2() && CommitChanges(false, values) && migration.Commit();
}

void DOMStorageDatabase::Close()
{
    db_.reset(NULL);
}

} // namespace content
